[slug] matches exactly one segment, [...slug] matches one or more segments, and [[...slug]] matches zero or more segments, with each syntax providing different levels of flexibility for handling URL paths
In Next.js dynamic routes, the number of brackets determines how many URL segments are captured. Single brackets [slug] capture exactly one segment; triple dots with brackets [...slug] (catch-all) capture one or more segments; and double brackets with triple dots [[...slug]] (optional catch-all) capture zero or more segments. These three syntaxes give you progressively more flexibility in handling variable-depth URLs, from fixed single segments to completely optional multi-segment paths.
[slug] (Single bracket): Matches exactly one segment. Example: /posts/hello-world → { slug: 'hello-world' } .
[...slug] (Triple dot with brackets): Matches one or more segments. Example: /docs/getting-started/installation → { slug: ['getting-started', 'installation'] } .
[[...slug]] (Double brackets with triple dots): Matches zero or more segments. Example: / → { slug: undefined } and /docs/getting-started → { slug: ['docs', 'getting-started'] } .
The [slug] pattern is the most common and simplest dynamic route. It captures exactly one segment from the URL. This is perfect for routes like blog posts (/posts/hello-world), product pages (/products/iphone-12), or user profiles (/users/john-doe). The parameter is always a string, and the route will only match if there is exactly one segment in that position. If there are zero or more than one segments, it won't match .
The [...slug] pattern captures one or more segments from that point forward in the URL. The parameter becomes an array of strings, with each segment as an element. This is ideal for documentation sites, category hierarchies, or any route that needs to handle variable-depth paths. The route requires at least one segment—if there are zero, it won't match .
The [[...slug]] pattern is the most flexible. It matches zero, one, or multiple segments. When there are no matching segments, the parameter becomes undefined. This is perfect for homepage variants, root-level catch-alls, or when you want the same page to handle both the base route and nested routes. The extra brackets make the entire segment optional .
The key difference between [...slug] and [[...slug]] is optionality. [...slug] always expects at least one segment—it's a required parameter. [[...slug]] makes the entire path segment optional, so it matches even when no segment is present. This distinction is crucial for root-level routing. For example, if you put [[...slug]] at your root app folder, it would match the homepage (/) as well as /blog, /products, etc., effectively overriding all other routes unless you're careful with ordering .
For [slug]: params: { slug: string } — always a string .
For [...slug]: params: { slug: string[] } — always an array with at least one element .
For [[...slug]]: params: { slug?: string[] } — optional array, could be undefined .
When using catch-all routes with other dynamic segments, be careful about route conflicts—more specific routes (like /[id]) should come before catch-all routes in the file system .
Each syntax serves distinct real-world needs: [slug] for blog posts and product pages where each URL corresponds to exactly one resource. [...slug] for documentation sections that can be nested arbitrarily deep, like /docs/api/v2/reference/methods. [[...slug]] for internationalized root routes where you want / and /en and /en/about all handled by the same layout, or for category pages where you want both /categories and /categories/electronics to work .